/*
 * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU.
 * Copyright (c) 2006-2018 Christian Walter <cwalter@embedded-solutions.at>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * File: $Id: mbrtu.c,v 1.18 2007/09/12 10:15:56 wolti Exp $
 */

/* ----------------------- System includes ----------------------------------*/
#include "stdlib.h"
#include "string.h"

/* ----------------------- Platform includes --------------------------------*/
#include "port.h"

/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbframe.h"
#include "mbrtu.h"

#include "mbcrc.h"
#include "mbport.h"

/* ----------------------- Defines ------------------------------------------*/
#define MB_SER_PDU_SIZE_MIN 4   /*!< Minimum size of a Modbus RTU frame. */
#define MB_SER_PDU_SIZE_MAX 256 /*!< Maximum size of a Modbus RTU frame. */
#define MB_SER_PDU_SIZE_CRC 2   /*!< Size of CRC field in PDU. */
#define MB_SER_PDU_ADDR_OFF 0   /*!< Offset of slave address in Ser-PDU. */
#define MB_SER_PDU_PDU_OFF 1    /*!< Offset of Modbus-PDU in Ser-PDU. */

/* ----------------------- Type definitions ---------------------------------*/
typedef enum {
  STATE_RX_INIT, /*!< Receiver is in initial state. */
  STATE_RX_IDLE, /*!< Receiver is in idle state. */
  STATE_RX_RCV,  /*!< Frame is beeing received. */
  STATE_RX_ERROR /*!< If the frame is invalid. */
} eMBRcvState;

typedef enum {
  STATE_TX_IDLE, /*!< Transmitter is in idle state. */
  STATE_TX_XMIT  /*!< Transmitter is in transfer state. */
} eMBSndState;

/* ----------------------- Static variables ---------------------------------*/
static volatile eMBSndState eSndState;
static volatile eMBRcvState eRcvState;

volatile UCHAR ucRTUBuf[MB_SER_PDU_SIZE_MAX];

static volatile UCHAR *pucSndBufferCur;
static volatile USHORT usSndBufferCount;

static volatile USHORT usRcvBufferPos;

/* ----------------------- Start implementation -----------------------------*/
eMBErrorCode eMBRTUInit(UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate,
                        eMBParity eParity) {
  eMBErrorCode eStatus = MB_ENOERR;
  ULONG usTimerT35_50us;

  (void)ucSlaveAddress;
  ENTER_CRITICAL_SECTION();

  /* Modbus RTU uses 8 Databits. */
  if (xMBPortSerialInit(ucPort, ulBaudRate, 8, eParity) != TRUE) {
    eStatus = MB_EPORTERR;
  } else {
    /* If baudrate > 19200 then we should use the fixed timer values
     * t35 = 1750us. Otherwise t35 must be 3.5 times the character time.
     */
    if (ulBaudRate > 19200) {
      usTimerT35_50us = 35; /* 1800us. */
    } else {
      /* The timer reload value for a character is given by:
       *
       * ChTimeValue = Ticks_per_1s / ( Baudrate / 11 )
       *             = 11 * Ticks_per_1s / Baudrate
       *             = 220000 / Baudrate
       * The reload for t3.5 is 1.5 times this value and similary
       * for t3.5.
       */
      usTimerT35_50us = (7UL * 220000UL) / (2UL * ulBaudRate);
    }
    if (xMBPortTimersInit((USHORT)usTimerT35_50us) != TRUE) {
      eStatus = MB_EPORTERR;
    }
  }
  EXIT_CRITICAL_SECTION();

  return eStatus;
}

void eMBRTUStart(void) {
  ENTER_CRITICAL_SECTION();
  /* Initially the receiver is in the state STATE_RX_INIT. we start
   * the timer and if no character is received within t3.5 we change
   * to STATE_RX_IDLE. This makes sure that we delay startup of the
   * modbus protocol stack until the bus is free.
   */
  eRcvState = STATE_RX_INIT;
  vMBPortSerialEnable(TRUE, FALSE);
  vMBPortTimersEnable();
  EXIT_CRITICAL_SECTION();
}

void eMBRTUStop(void) {
  ENTER_CRITICAL_SECTION();
  vMBPortSerialEnable(FALSE, FALSE);
  vMBPortTimersDisable();
  EXIT_CRITICAL_SECTION();
}

eMBErrorCode eMBRTUReceive(UCHAR *pucRcvAddress, UCHAR **pucFrame,
                           USHORT *pusLength) {
  eMBErrorCode eStatus = MB_ENOERR;

  ENTER_CRITICAL_SECTION();
  assert_param(usRcvBufferPos < MB_SER_PDU_SIZE_MAX);
  /* Length and CRC check */
  if ((usRcvBufferPos >= MB_SER_PDU_SIZE_MIN) &&
      (usMBCRC16((UCHAR *)ucRTUBuf, usRcvBufferPos) == 0)) {
    /* Save the address field. All frames are passed to the upper layed
     * and the decision if a frame is used is done there.
     */
    *pucRcvAddress = ucRTUBuf[MB_SER_PDU_ADDR_OFF];

    /* Total length of Modbus-PDU is Modbus-Serial-Line-PDU minus
     * size of address field and CRC checksum.
     */
    *pusLength =
        (USHORT)(usRcvBufferPos - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC);

    /* Return the start of the Modbus PDU to the caller. */
    *pucFrame = (UCHAR *)&ucRTUBuf[MB_SER_PDU_PDU_OFF];
  } else {
    eStatus = MB_EIO;
  }

  EXIT_CRITICAL_SECTION();
  return eStatus;
}

eMBErrorCode eMBRTUSend(UCHAR ucSlaveAddress, const UCHAR *pucFrame,
                        USHORT usLength) {
  eMBErrorCode eStatus = MB_ENOERR;
  USHORT usCRC16;

  ENTER_CRITICAL_SECTION();

  /* Check if the receiver is still in idle state. If not we where to
   * slow with processing the received frame and the master sent another
   * frame on the network. We have to abort sending the frame.
   */
  if (eRcvState == STATE_RX_IDLE) {
    /* First byte before the Modbus-PDU is the slave address. */
    pucSndBufferCur = (UCHAR *)pucFrame - 1;
    usSndBufferCount = 1;

    /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */
    pucSndBufferCur[MB_SER_PDU_ADDR_OFF] = ucSlaveAddress;
    usSndBufferCount += usLength;

    /* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */
    usCRC16 = usMBCRC16((UCHAR *)pucSndBufferCur, usSndBufferCount);
    ucRTUBuf[usSndBufferCount++] = (UCHAR)(usCRC16 & 0xFF);
    ucRTUBuf[usSndBufferCount++] = (UCHAR)(usCRC16 >> 8);

    /* Activate the transmitter. */
    eSndState = STATE_TX_XMIT;
    vMBPortSerialEnable(FALSE, TRUE);
  } else {
    eStatus = MB_EIO;
  }
  EXIT_CRITICAL_SECTION();
  return eStatus;
}

BOOL xMBRTUReceiveFSM(void) {
  BOOL xTaskNeedSwitch = FALSE;
  UCHAR ucByte;
  assert_param(eSndState == STATE_TX_IDLE);

  /* Always read the character. */
  (void)xMBPortSerialGetByte((CHAR *)&ucByte);

  switch (eRcvState) {
    /* If we have received a character in the init state we have to
     * wait until the frame is finished.
     */
  case STATE_RX_INIT:
    vMBPortTimersEnable();
    break;

    /* In the error state we wait until all characters in the
     * damaged frame are transmitted.
     */
  case STATE_RX_ERROR:
    vMBPortTimersEnable();
    break;

    /* In the idle state we wait for a new character. If a character
     * is received the t1.5 and t3.5 timers are started and the
     * receiver is in the state STATE_RX_RECEIVCE.
     */
  case STATE_RX_IDLE:
    usRcvBufferPos = 0;
    ucRTUBuf[usRcvBufferPos++] = ucByte;
    eRcvState = STATE_RX_RCV;

    /* Enable t3.5 timers. */
    vMBPortTimersEnable();
    break;

    /* We are currently receiving a frame. Reset the timer after
     * every character received. If more than the maximum possible
     * number of bytes in a modbus frame is received the frame is
     * ignored.
     */
  case STATE_RX_RCV:
    if (usRcvBufferPos < MB_SER_PDU_SIZE_MAX) {
      ucRTUBuf[usRcvBufferPos++] = ucByte;
    } else {
      eRcvState = STATE_RX_ERROR;
    }
    vMBPortTimersEnable();
    break;
  }
  return xTaskNeedSwitch;
}

BOOL xMBRTUTransmitFSM(void) {
  BOOL xNeedPoll = FALSE;

  assert_param(eRcvState == STATE_RX_IDLE);

  switch (eSndState) {
    /* We should not get a transmitter event if the transmitter is in
     * idle state.  */
  case STATE_TX_IDLE:
    /* enable receiver/disable transmitter. */
    vMBPortSerialEnable(TRUE, FALSE);
    break;

  case STATE_TX_XMIT:
    /* check if we are finished. */
    if (usSndBufferCount != 0) {
      xMBPortSerialPutByte((CHAR)*pucSndBufferCur);
      pucSndBufferCur++; /* next byte in sendbuffer. */
      usSndBufferCount--;
    } else {
      xNeedPoll = xMBPortEventPost(EV_FRAME_SENT);
      /* Disable transmitter. This prevents another transmit buffer
       * empty interrupt. */
      vMBPortSerialEnable(TRUE, FALSE);
      eSndState = STATE_TX_IDLE;
    }
    break;
  }

  return xNeedPoll;
}

BOOL xMBRTUTimerT35Expired(void) {
  BOOL xNeedPoll = FALSE;

  switch (eRcvState) {
    /* Timer t35 expired. Startup phase is finished. */
  case STATE_RX_INIT:
    xNeedPoll = xMBPortEventPost(EV_READY);
    break;

    /* A frame was received and t35 expired. Notify the listener that
     * a new frame was received. */
  case STATE_RX_RCV:
    xNeedPoll = xMBPortEventPost(EV_FRAME_RECEIVED);
    break;

    /* An error occured while receiving the frame. */
  case STATE_RX_ERROR:
    break;

    /* Function called in an illegal state. */
  default:
    assert_param((eRcvState == STATE_RX_INIT) || (eRcvState == STATE_RX_RCV) ||
                 (eRcvState == STATE_RX_ERROR));
    break;
  }
  vMBPortTimersDisable();
  eRcvState = STATE_RX_IDLE;
  return xNeedPoll;
}
